/*
 * Copyright 2006-2018 The MZmine 2 Development Team
 * 
 * This file is part of MZmine 2.
 * 
 * MZmine 2 is free software; you can redistribute it and/or modify it under the terms of the GNU
 * General Public License as published by the Free Software Foundation; either version 2 of the
 * License, or (at your option) any later version.
 * 
 * MZmine 2 is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without
 * even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
 * General Public License for more details.
 * 
 * You should have received a copy of the GNU General Public License along with MZmine 2; if not,
 * write to the Free Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301
 * USA
 */

package net.sf.mzmine.modules.visualization.histogram;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Vector;

import net.sf.mzmine.datamodel.Feature;
import net.sf.mzmine.datamodel.PeakList;
import net.sf.mzmine.datamodel.RawDataFile;

import org.jfree.data.general.DatasetChangeEvent;
import org.jfree.data.statistics.HistogramBin;
import org.jfree.data.statistics.HistogramType;
import org.jfree.data.xy.AbstractIntervalXYDataset;
import org.jfree.data.xy.IntervalXYDataset;

import com.google.common.collect.Range;

public class HistogramPlotDataset extends AbstractIntervalXYDataset {

  private static final long serialVersionUID = 1L;
  private HistogramDataType dataType;
  private PeakList peakList;
  private RawDataFile[] rawDataFiles;
  private int numOfBins;
  private double maximum, minimum;

  /** A list of maps. */
  private Vector<HashMap<?, ?>> list;

  /** The histogram type. */
  private HistogramType type;

  public HistogramPlotDataset(PeakList peakList, RawDataFile[] rawDataFiles, int numOfBins,
      HistogramDataType dataType, Range<Double> range) {

    this.list = new Vector<HashMap<?, ?>>();
    this.type = HistogramType.FREQUENCY;
    this.dataType = dataType;
    this.peakList = peakList;
    this.numOfBins = numOfBins;
    this.rawDataFiles = rawDataFiles;

    minimum = range.lowerEndpoint();
    maximum = range.upperEndpoint();

    updateHistogramDataset();

  }

  public void updateHistogramDataset() {
    this.list.clear();
    Feature[] peaks;
    double[] values = null;
    for (RawDataFile dataFile : rawDataFiles) {
      peaks = peakList.getPeaks(dataFile);
      values = new double[peaks.length];
      for (int i = 0; i < peaks.length; i++) {
        switch (dataType) {
          case AREA:
            values[i] = peaks[i].getArea();
            break;
          case HEIGHT:
            values[i] = peaks[i].getHeight();
            break;
          case MASS:
            values[i] = peaks[i].getMZ();
            break;
          case RT:
            values[i] = peaks[i].getRT();
            break;
        }

      }
      addSeries(dataFile.getName(), values);
    }

  }

  public void setNumberOfBins(int numOfBins) {
    this.numOfBins = numOfBins;
  }

  public int getNumberOfBins() {
    return this.numOfBins;
  }

  public void setBinWidth(double binWidth) {
    int numBins;
    updateHistogramDataset();
    double[] values = getValues(0);
    double minimum = getMinimum(values);
    double maximum = getMaximum(values);
    numBins = (int) ((maximum - minimum) / binWidth);
    setNumberOfBins(numBins);
  }

  public double getBinWidth() {
    return this.getBinWidth(0);
  }

  public void setHistogramDataType(HistogramDataType dataType) {
    this.dataType = dataType;
  }

  public PeakList getPeakList() {
    return this.peakList;
  }

  /**
   * Returns the histogram type.
   * 
   * @return The type (never <code>null</code>).
   */
  public HistogramType getType() {
    return this.type;
  }

  /**
   * Sets the histogram type and sends a {@link DatasetChangeEvent} to all registered listeners.
   * 
   * @param type the type (<code>null</code> not permitted).
   */
  public void setType(HistogramType type) {
    if (type == null) {
      throw new IllegalArgumentException("Null 'type' argument");
    }
    this.type = type;
    notifyListeners(new DatasetChangeEvent(this, this));
  }

  /**
   * Adds a series to the dataset. Any data value less than minimum will be assigned to the first
   * bin, and any data value greater than maximum will be assigned to the last bin. Values falling
   * on the boundary of adjacent bins will be assigned to the higher indexed bin.
   * 
   * @param key the series key (<code>null</code> not permitted).
   * @param values the raw observations.
   * @param numOfBins the number of bins (must be at least 1).
   * @param minimum the lower bound of the bin range.
   * @param maximum the upper bound of the bin range.
   */
  public void addSeries(Comparable<?> key, double[] values) {

    if (key == null) {
      throw new IllegalArgumentException("Null 'key' argument.");
    }
    if (values == null) {
      throw new IllegalArgumentException("Null 'values' argument.");
    } else if (numOfBins < 1) {
      throw new IllegalArgumentException("The 'bins' value must be at least 1.");
    }
    double binWidth = (maximum - minimum) / numOfBins;

    double lower = minimum;
    double upper;
    List<HistogramBin> binList = new ArrayList<HistogramBin>(numOfBins);
    for (int i = 0; i < numOfBins; i++) {
      HistogramBin bin;
      // make sure bins[bins.length]'s upper boundary ends at maximum
      // to avoid the rounding issue. the bins[0] lower boundary is
      // guaranteed start from min
      if (i == numOfBins - 1) {
        bin = new HistogramBin(lower, maximum);
      } else {
        upper = minimum + (i + 1) * binWidth;
        bin = new HistogramBin(lower, upper);
        lower = upper;
      }
      binList.add(bin);
    }
    // fill the bins
    for (int i = 0; i < values.length; i++) {
      int binIndex = numOfBins - 1;
      if (values[i] < maximum) {
        double fraction = (values[i] - minimum) / (maximum - minimum);
        if (fraction < 0.0) {
          fraction = 0.0;
        }
        binIndex = (int) (fraction * numOfBins);
        if (binIndex >= numOfBins) {
          binIndex = numOfBins - 1;
        }
      }
      HistogramBin bin = (HistogramBin) binList.get(binIndex);
      bin.incrementCount();

    }
    // generic map for each series
    HashMap<String, Object> map = new HashMap<String, Object>();
    map.put("key", key);
    map.put("bins", binList);
    map.put("values.length", new Integer(values.length));
    map.put("bin width", new Double(binWidth));
    map.put("values", values);
    this.list.add(map);
  }

  /**
   * Returns the minimum value in an array of values.
   * 
   * @param values the values (<code>null</code> not permitted and zero-length array not permitted).
   * 
   * @return The minimum value.
   */
  private double getMinimum(double[] values) {
    if (values == null || values.length < 1) {
      throw new IllegalArgumentException("Null or zero length 'values' argument.");
    }
    double min = Double.MAX_VALUE;
    for (int i = 0; i < values.length; i++) {
      if (values[i] < min) {
        min = values[i];
      }
    }
    return min;
  }

  /**
   * Returns the maximum value in an array of values.
   * 
   * @param values the values (<code>null</code> not permitted and zero-length array not permitted).
   * 
   * @return The maximum value.
   */
  private double getMaximum(double[] values) {
    if (values == null || values.length < 1) {
      throw new IllegalArgumentException("Null or zero length 'values' argument.");
    }
    double max = -Double.MAX_VALUE;
    for (int i = 0; i < values.length; i++) {
      if (values[i] > max) {
        max = values[i];
      }
    }
    return max;
  }

  /**
   * Returns the bins for a series.
   * 
   * @param series the series index (in the range <code>0</code> to
   *        <code>getSeriesCount() - 1</code>).
   * 
   * @return A list of bins.
   * 
   * @throws IndexOutOfBoundsException if <code>series</code> is outside the specified range.
   */
  private List<?> getBins(int series) {
    HashMap<?, ?> map = (HashMap<?, ?>) this.list.get(series);
    return (List<?>) map.get("bins");
  }

  /**
   * @param series
   * @return
   */
  private double[] getValues(int series) {
    HashMap<?, ?> map = (HashMap<?, ?>) this.list.get(series);
    return (double[]) map.get("values");
  }

  /**
   * Returns the total number of observations for a series.
   * 
   * @param series the series index.
   * 
   * @return The total.
   */
  private int getTotal(int series) {
    Map<?, ?> map = (Map<?, ?>) this.list.get(series);
    return ((Integer) map.get("values.length")).intValue();
  }

  /**
   * Returns the bin width for a series.
   * 
   * @param series the series index (zero based).
   * 
   * @return The bin width.
   */
  private double getBinWidth(int series) {
    Map<?, ?> map = (Map<?, ?>) this.list.get(series);
    return ((Double) map.get("bin width")).doubleValue();
  }

  /**
   * Returns the number of series in the dataset.
   * 
   * @return The series count.
   */
  public int getSeriesCount() {
    return this.list.size();
  }

  /**
   * Returns the key for a series.
   * 
   * @param series the series index (in the range <code>0</code> to
   *        <code>getSeriesCount() - 1</code>).
   * 
   * @return The series key.
   * 
   * @throws IndexOutOfBoundsException if <code>series</code> is outside the specified range.
   */
  public Comparable<?> getSeriesKey(int series) {
    Map<?, ?> map = (Map<?, ?>) this.list.get(series);
    return (Comparable<?>) map.get("key");
  }

  /**
   * Returns the number of data items for a series.
   * 
   * @param series the series index (in the range <code>0</code> to
   *        <code>getSeriesCount() - 1</code>).
   * 
   * @return The item count.
   * 
   * @throws IndexOutOfBoundsException if <code>series</code> is outside the specified range.
   */

  public int getItemCount(int series) {
    return getBins(series).size();
  }

  /**
   * Returns the X value for a bin. This value won't be used for plotting histograms, since the
   * renderer will ignore it. But other renderers can use it (for example, you could use the dataset
   * to create a line chart).
   * 
   * @param series the series index (in the range <code>0</code> to
   *        <code>getSeriesCount() - 1</code>).
   * @param item the item index (zero based).
   * 
   * @return The start value.
   * 
   * @throws IndexOutOfBoundsException if <code>series</code> is outside the specified range.
   */
  public Number getX(int series, int item) {
    List<?> bins = getBins(series);
    HistogramBin bin = (HistogramBin) bins.get(item);
    double x = (bin.getStartBoundary() + bin.getEndBoundary()) / 2.;
    return new Double(x);
  }

  /**
   * Returns the y-value for a bin (calculated to take into account the histogram type).
   * 
   * @param series the series index (in the range <code>0</code> to
   *        <code>getSeriesCount() - 1</code>).
   * @param item the item index (zero based).
   * 
   * @return The y-value.
   * 
   * @throws IndexOutOfBoundsException if <code>series</code> is outside the specified range.
   */
  public Number getY(int series, int item) {
    List<?> bins = getBins(series);
    HistogramBin bin = (HistogramBin) bins.get(item);
    double total = getTotal(series);
    double binWidth = getBinWidth(series);

    if (this.type == HistogramType.FREQUENCY) {
      return new Double(bin.getCount());
    } else if (this.type == HistogramType.RELATIVE_FREQUENCY) {
      return new Double(bin.getCount() / total);
    } else if (this.type == HistogramType.SCALE_AREA_TO_1) {
      return new Double(bin.getCount() / (binWidth * total));
    } else { // pretty sure this shouldn't ever happen
      throw new IllegalStateException();
    }
  }

  /**
   * Returns the start value for a bin.
   * 
   * @param series the series index (in the range <code>0</code> to
   *        <code>getSeriesCount() - 1</code>).
   * @param item the item index (zero based).
   * 
   * @return The start value.
   * 
   * @throws IndexOutOfBoundsException if <code>series</code> is outside the specified range.
   */
  public Number getStartX(int series, int item) {
    List<?> bins = getBins(series);
    HistogramBin bin = (HistogramBin) bins.get(item);
    return new Double(bin.getStartBoundary());
  }

  /**
   * Returns the end value for a bin.
   * 
   * @param series the series index (in the range <code>0</code> to
   *        <code>getSeriesCount() - 1</code>).
   * @param item the item index (zero based).
   * 
   * @return The end value.
   * 
   * @throws IndexOutOfBoundsException if <code>series</code> is outside the specified range.
   */
  public Number getEndX(int series, int item) {
    List<?> bins = getBins(series);
    HistogramBin bin = (HistogramBin) bins.get(item);
    return new Double(bin.getEndBoundary());
  }

  /**
   * Returns the start y-value for a bin (which is the same as the y-value, this method exists only
   * to support the general form of the {@link IntervalXYDataset} interface).
   * 
   * @param series the series index (in the range <code>0</code> to
   *        <code>getSeriesCount() - 1</code>).
   * @param item the item index (zero based).
   * 
   * @return The y-value.
   * 
   * @throws IndexOutOfBoundsException if <code>series</code> is outside the specified range.
   */
  public Number getStartY(int series, int item) {
    return getY(series, item);
  }

  /**
   * Returns the end y-value for a bin (which is the same as the y-value, this method exists only to
   * support the general form of the {@link IntervalXYDataset} interface).
   * 
   * @param series the series index (in the range <code>0</code> to
   *        <code>getSeriesCount() - 1</code>).
   * @param item the item index (zero based).
   * 
   * @return The Y value.
   * 
   * 
   * @throws IndexOutOfBoundsException if <code>series</code> is outside the specified range.
   */
  public Number getEndY(int series, int item) {
    return getY(series, item);
  }

  public double getMinimum() {
    return minimum;
  }

  public double getMaximum() {
    return maximum;
  }

}
